#include "../window.h"

#ifndef DIRECTINPUT_VERSION
#define DIRECTINPUT_VERSION 0x0800
#endif
#include <dinput.h>

#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "dinput8.lib")
#pragma comment(lib, "dxerr.lib")
#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "delayimp.lib")

#include "../window_common_impl.h"

#include "tchar.h"

#define PIMPL (*self->internal_)

#define FLIP_INPUT_BUFFER_SIZE 32

typedef struct flip_window_internal_
{
	flip_window_internal_common common;
	
	HWND hwnd;
	HDC hdc;
	int closed;
	int iconified;
	int swap_mouse;
	
	IDirectInput8* input;
	IDirectInputDevice8* keyboard;
	IDirectInputDevice8* mouse;
} flip_window_internal;

static LPCTSTR window_class_name = NULL;

static HRESULT setup_input(flip_window_internal* self);

static LRESULT handle_message(
	flip_window_internal* self,
	UINT message,
	WPARAM wparam,
	LPARAM lparam)
{
	if(message == WM_SETFOCUS && self->common.fullscreen && IsWindowVisible(self->hwnd))
    {
        if(self->iconified)
        {
            uint w = self->common.width, h = self->common.height, bpp = 32, rr = 60;
            OpenIcon(self->hwnd);
            
            // TODO: setVideoMode(findClosestVideoMode(&w, &h, &bpp, &rr));
            self->iconified = 0;
        }
        return 0;
    }

    if(message == WM_KILLFOCUS
    && self->common.fullscreen
    && IsWindowVisible(self->hwnd))
    {
        if(!self->iconified)
        {
            ChangeDisplaySettings(NULL, CDS_FULLSCREEN);
            CloseWindow(self->hwnd);
            self->iconified = 1;
        }
        return 0;
    }
    
    if (message == WM_SETCURSOR && LOWORD(lparam) == HTCLIENT)
    {
        SetCursor(0);
        return TRUE;
    }
    
	if(message == WM_CLOSE)
    {
        flip_window_close(self->common.parent);
        return 0;
    }
    
	if(message == WM_PAINT)
    {
		int swap = 0;
		
		flip_window_draw(self->common.parent);
        
		if(swap)
		{
			glFlush();
			SwapBuffers(self->hdc);
		}
        ValidateRect(self->hwnd, 0);
        return 0;
    }

    return DefWindowProc(self->hwnd, message, wparam, lparam);
}

static LRESULT CALLBACK window_proc(HWND wnd, UINT message, WPARAM wparam, LPARAM lparam)
{
	LONG_PTR lptr = GetWindowLongPtr(wnd, GWLP_USERDATA);

	if (0 != lptr)
	{
		flip_window_internal* obj = (flip_window_internal*)(lptr);
		return handle_message(obj, message, wparam, lparam);
	}
	else
		return DefWindowProc(wnd, message, wparam, lparam);
}

static void process_messages()
{
	MSG message;

	while (PeekMessage(&message, 0, 0, 0, PM_REMOVE))
	{
		//if (!handledByHook(message))
		{
			TranslateMessage(&message);
			DispatchMessage(&message);
		}
	}
	
}

static LPCTSTR window_class()
{
	WNDCLASS wc;
	
	if (NULL != window_class_name)
		return window_class_name;
	
	wc.lpszClassName = _T("Flip::Window");
	wc.style = CS_OWNDC;
	wc.lpfnWndProc = window_proc;
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = GetModuleHandle(0);
	wc.hIcon = 0;
	wc.hCursor = 0;
	wc.hbrBackground = 0;
	wc.lpszMenuName = 0;
	
	window_class_name = (LPCTSTR)(RegisterClass(&wc));
	//check(name, "registering a window class");
	return window_class_name;
}

static flip_window_internal*
flip_create_window_internal()
{
	flip_window_internal* w = malloc(sizeof(flip_window_internal));
	
	flip_window_internal_common_init(&w->common);
	w->hwnd = NULL;
	w->hdc = NULL;
	w->closed = 0;
	w->iconified = 0;
	
	w->input = NULL;
	w->keyboard = NULL;
	w->mouse = NULL;
	w->swap_mouse = 0;
	return w;
}


static void flip_destroy_window_internal(flip_window_internal* self)
{
	if(self->hdc)
		ReleaseDC(self->hwnd, self->hdc);
	if(self->hwnd)
		DestroyWindow(self->hwnd);
	free(self);
}


// Mode guessing experimentally adapted from GLFW library.
// http://glfw.sourceforge.net/

static int find_closest_video_mode(int *w, int *h, int *bpp, int *refresh)
{
    int mode, bestmode, match, bestmatch, rr, bestrr, success;
    DEVMODE dm;

    // Find best match
    bestmatch = 0x7fffffff;
    bestrr    = 0x7fffffff;
    mode = bestmode = 0;
    do
    {
        dm.dmSize = sizeof(DEVMODE);
        success = EnumDisplaySettings(NULL, mode, &dm);
        if( success )
        {
            match = dm.dmBitsPerPel - *bpp;
            if( match < 0 ) match = -match;
            match = (match << 25) |
                    ((dm.dmPelsWidth - *w) * (dm.dmPelsWidth - *w) +
                     (dm.dmPelsHeight - *h) * (dm.dmPelsHeight - *h));
            if( match < bestmatch )
            {
                bestmatch = match;
                bestmode  = mode;
                bestrr = (dm.dmDisplayFrequency - *refresh) *
                         (dm.dmDisplayFrequency - *refresh);
            }
            else if( match == bestmatch && *refresh > 0 )
            {
                rr = (dm.dmDisplayFrequency - *refresh) *
                     (dm.dmDisplayFrequency - *refresh);
                if( rr < bestrr )
                {
                    bestmatch = match;
                    bestmode  = mode;
                    bestrr    = rr;
                }
            }
        }
        ++mode;
    }
    while (success);

    // Get the parameters for the best matching display mode
    dm.dmSize = sizeof(DEVMODE);
    EnumDisplaySettings( NULL, bestmode, &dm );

    *w = dm.dmPelsWidth;
    *h = dm.dmPelsHeight;
    *bpp = dm.dmBitsPerPel;
    *refresh = dm.dmDisplayFrequency;

    return bestmode;
}

static int set_video_mode(int mode)
{
    // Get the parameters
    DEVMODE dm;
    dm.dmSize = sizeof(DEVMODE);
    EnumDisplaySettings(NULL, mode, &dm);

    // Set which fields we want to specify
    dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;

    // Change display setting
    dm.dmSize = sizeof(DEVMODE);
    if (ChangeDisplaySettings(&dm, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
        return -1;
    return 0;
}

static void setup_vsync(flip_window_internal* w)
{
	typedef void (APIENTRY *PFNWGLEXTSWAPCONTROLPROC) (int);
	PFNWGLEXTSWAPCONTROLPROC wglSwapIntervalEXT;
	
	if(!flip_window_extension_supported(w->common.parent, "WGL_EXT_swap_control"))
		return;
		
	
	wglSwapIntervalEXT =
		(PFNWGLEXTSWAPCONTROLPROC) wglGetProcAddress("wglSwapIntervalEXT");
	if (!wglSwapIntervalEXT)
		return;
	wglSwapIntervalEXT(1);
}

static void set_mode(
	flip_window_internal* self,
	uint width,
	uint height,
	int fullscreen)
{
	if(!self->hwnd)
	{
		DWORD style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN;
		DWORD styleEx = WS_EX_APPWINDOW;
		PIXELFORMATDESCRIPTOR pfd;
		RECT rc;
		unsigned window_x, window_y;
		unsigned window_w, window_h;
		int pf;
		HGLRC hrc;
		
		/* Initialize common first */
		self->common.width = width;
		self->common.height = height;
		self->common.fullscreen = fullscreen;
		
		if(fullscreen)
		{
			style |= WS_POPUP;
			styleEx |= WS_EX_TOPMOST;
		}
		else
		{
			style |= WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
			styleEx |= WS_EX_WINDOWEDGE;
		}

		self->hwnd = CreateWindowEx(styleEx, window_class(), 0, style,
			CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0,
			GetModuleHandle(0), 0);
		//Impl::check(PIMPL.hwnd);

		self->hdc = GetDC(self->hwnd);
		//Impl::check(PIMPL.hdc);

		
		ZeroMemory(&pfd, sizeof pfd);
		pfd.nSize        = sizeof pfd;
		pfd.nVersion     = 1;
		pfd.dwFlags      = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
		pfd.iLayerType   = PFD_MAIN_PLANE;
		pfd.iPixelType   = PFD_TYPE_RGBA;
		pfd.cColorBits   = 32;
		pf = ChoosePixelFormat(self->hdc, &pfd);
		//Impl::check(pf);
		//Impl::check(SetPixelFormat(PIMPL.hdc, pf, &pfd));
		SetPixelFormat(self->hdc, pf, &pfd);

		//HGLRC hrc = Impl::check(wglCreateContext(PIMPL.hdc), "creating rendering context");
		hrc = wglCreateContext(self->hdc);
		//Impl::check(wglMakeCurrent(PIMPL.hdc, hrc), "selecting the rendering context");
		wglMakeCurrent(self->hdc, hrc);
		
		flip_window_internal_setup_gl(&self->common);

		SetLastError(0);
		SetWindowLongPtr(self->hwnd, GWLP_USERDATA,
			(LONG_PTR)(self));
			
#if 0			
		if(GetLastError() != 0)
			Impl::throwLastError("setting the window's GWLP_USERDATA pointer");
#endif

		// Determine the size the window needs to have.
		rc.top = 0;
		rc.left = 0;
		rc.right = width;
		rc.bottom = height;
		AdjustWindowRectEx(&rc, style, FALSE, styleEx);
		window_w = rc.right - rc.left;
		window_h = rc.bottom - rc.top;

		window_x = 0;
		window_y = 0;

		if(!self->common.fullscreen)
		{
			// Center the window.
			HWND desktopWindow = GetDesktopWindow();
			RECT desktopRect;
			int desktop_w, desktop_h;
			
			GetClientRect(desktopWindow, &desktopRect);
			desktop_w = desktopRect.right - desktopRect.left;
			desktop_h = desktopRect.bottom - desktopRect.top;
			window_x = (desktop_w - window_w) / 2;
			window_y = (desktop_h - window_h) / 2;
		}

		MoveWindow(self->hwnd, window_x, window_y, window_w, window_h, 0);
		
		setup_input(self);
		setup_vsync(self);
		//PIMPL.setupVSync();
		
	}
}

#define CHECK_DI(name, x) do { HRESULT r_ = (x); if(FAILED(r_)) return r_; } while(0)

static HRESULT setup_input(flip_window_internal* self)
{
	IDirectInput8* inputRaw;
	IDirectInputDevice8* mouseRaw;
	IDirectInputDevice8* kbRaw;
	DIPROPDWORD bufferSize;
	
	CHECK_DI("creating the main DirectInput object",
		DirectInput8Create(GetModuleHandle(0), DIRECTINPUT_VERSION,
			&IID_IDirectInput8, (void**)(&inputRaw), 0));
	self->input = inputRaw;

	// Prepare property struct for setting the amount of data to buffer.

	
	bufferSize.diph.dwSize = sizeof(DIPROPDWORD);
	bufferSize.diph.dwHeaderSize = sizeof(DIPROPHEADER);
	bufferSize.diph.dwHow = DIPH_DEVICE;
	bufferSize.diph.dwObj = 0;
	bufferSize.dwData = FLIP_INPUT_BUFFER_SIZE;


	// Set up the system keyboard.
	
	CHECK_DI("creating the keyboard device object",
		IDirectInput8_CreateDevice(self->input, &GUID_SysKeyboard, &kbRaw, 0));
	self->keyboard = kbRaw;
	
	

	CHECK_DI("setting the keyboard's data format",
		IDirectInputDevice8_SetDataFormat(kbRaw, &c_dfDIKeyboard));
	CHECK_DI("setting the keyboard's cooperative level",
		IDirectInputDevice8_SetCooperativeLevel(kbRaw, self->hwnd,
			DISCL_FOREGROUND | DISCL_NONEXCLUSIVE));
	CHECK_DI("setting the keyboard's buffer size",
		IDirectInputDevice8_SetProperty(kbRaw, DIPROP_BUFFERSIZE, &bufferSize.diph));
		
	IDirectInputDevice8_Acquire(kbRaw);
    
	// Set up the system mouse.
	
	CHECK_DI("creating the mouse device object",
		IDirectInput8_CreateDevice(self->input, &GUID_SysMouse, &mouseRaw, 0));
	self->mouse = mouseRaw;

	CHECK_DI("setting the mouse's data format",
		IDirectInputDevice8_SetDataFormat(mouseRaw, &c_dfDIMouse));
	CHECK_DI("setting the mouse's cooperative level",
		IDirectInputDevice8_SetCooperativeLevel(mouseRaw, self->hwnd,
			DISCL_FOREGROUND | DISCL_NONEXCLUSIVE));
	CHECK_DI("setting the mouse's buffer size",
		IDirectInputDevice8_SetProperty(mouseRaw, DIPROP_BUFFERSIZE, &bufferSize.diph));

	IDirectInputDevice8_Acquire(mouseRaw);

	self->swap_mouse = (GetSystemMetrics(SM_SWAPBUTTON) != 0);
	
	return ERROR_SUCCESS;
}

static void update_di_buttons(flip_window_internal* self, int collect_events)
{
	DIDEVICEOBJECTDATA data[FLIP_INPUT_BUFFER_SIZE];
	DWORD in_out;
	HRESULT hr;
	unsigned i;

	in_out = FLIP_INPUT_BUFFER_SIZE;
	hr = IDirectInputDevice8_GetDeviceData(self->mouse, sizeof data[0], data, &in_out, 0);
	switch(hr)
	{
		case DI_OK:
		case DI_BUFFEROVERFLOW:
		{
			// Everything's ok: Update buttons and fire events.
			for (i = 0; i < in_out; ++i)
			{
				int down = (data[i].dwData & 0x80) != 0;
                    
				switch (data[i].dwOfs)
				{
					case DIMOFS_BUTTON0:
					{
						unsigned id = self->swap_mouse ? flip_msRight : flip_msLeft;
						flip_window_force_button(self->common.parent, id, down, collect_events); // TODO: Why is forceButton used rather than setButton?
						break;
					}
					case DIMOFS_BUTTON1:
					{
						unsigned id = self->swap_mouse ? flip_msLeft : flip_msRight;
						flip_window_force_button(self->common.parent, id, down, collect_events);
						break;
					}
					case DIMOFS_BUTTON2:
					{
						flip_window_force_button(self->common.parent, flip_msMiddle, down, collect_events);
						break;
					}
					case DIMOFS_Z:
					{
						if (!collect_events || data[i].dwData == 0)
							break;

						/* TODO
						EventInfo event;
						event.action = EventInfo::buttonDown;
						if (int(data[i].dwData) < 0)
							event.id = msWheelDown;
						else
							event.id = msWheelUp;
						events.push_back(event);
						break;*/
					}
				}
			}
			break;
			
		}

		case DIERR_NOTACQUIRED:
		case DIERR_INPUTLOST:
		{
			unsigned id;
			// Cannot fetch new events: Release all buttons.
			for (id = flip_msRangeBegin; id < flip_msRangeEnd; ++id)
				flip_window_set_button(self->common.parent, id, 0, collect_events);
			IDirectInputDevice8_Acquire(self->mouse);
			break;
		}
	}

	in_out = FLIP_INPUT_BUFFER_SIZE;
	
	hr = IDirectInputDevice8_GetDeviceData(self->keyboard, sizeof data[0], data, &in_out, 0);
	switch (hr)
	{
		case DI_OK:
		case DI_BUFFEROVERFLOW:
		{
			for (i = 0; i < in_out; ++i)
				flip_window_force_button(self->common.parent, flip_keys_native_to_flip[data[i].dwOfs], (data[i].dwData & 0x80) != 0, collect_events);
			break;
		}

		case DIERR_NOTACQUIRED:
		case DIERR_INPUTLOST:
		{
			unsigned id;
			for (id = flip_kbRangeBegin; id < flip_kbRangeEnd; ++id)
				flip_window_set_button(self->common.parent, id, 0, collect_events);
			IDirectInputDevice8_Acquire(self->keyboard);
			break;
		}
	}
}

static void update_mouse_pos(flip_window_internal* self)
{
	POINT pos;
	GetCursorPos(&pos);
	ScreenToClient(self->hwnd, &pos);

	self->common.mouse_pos.x = (int)(pos.x);
	self->common.mouse_pos.y = (int)(pos.y);
}


void flip_window_end_drawing(flip_window* self)
{
	glFlush();
	SwapBuffers(self->internal_->hdc);
}


int flip_window_redraw(flip_window* self)
{
	InvalidateRect(PIMPL.hwnd, 0, FALSE);
	return 0;
}

int flip_window_set_visible(flip_window* self, int visible)
{
	if(!!IsWindowVisible(PIMPL.hwnd) == visible)
		return 0; // Already in the right state
		
	if(visible)
	{
		int w = PIMPL.common.width, h = PIMPL.common.height, bpp = 32, rr = 60;
		if(PIMPL.common.fullscreen)
		{
			set_video_mode(find_closest_video_mode(&w, &h, &bpp, &rr));
		}
	}
	else
	{
		if(PIMPL.common.fullscreen)
			ChangeDisplaySettings(NULL, CDS_FULLSCREEN);
	}
		
	ShowWindow(PIMPL.hwnd, visible ? SW_SHOW : SW_HIDE);
	return 0;
}

void flip_window_set_caption(flip_window* self, char const* str)
{
	SetWindowText(PIMPL.hwnd, str);
}

int flip_window_update(flip_window* self)
{
	process_messages(self->internal_);
	
	update_mouse_pos(self->internal_);
	update_di_buttons(self->internal_, 1);
	
	/* TODO
	Impl::Events events;
    events.swap(PIMPL.events);
    for (unsigned i = 0; i < events.size(); ++i)
    {
        if (events[i].action == Impl::EventInfo::buttonDown)
        {
            if (onButtonDown)
                onButtonDown(Button(events[i].id));
        }
        else
        {
            if (onButtonUp)
                onButtonUp(Button(events[i].id));
        }
    }*/
        
	return PIMPL.closed ? 1 : 0;
}

int flip_window_close(flip_window* self)
{
	flip_window_set_visible(self, 0);
	PIMPL.closed = 1;
	return 0;
}

#undef PIMPL
